# -*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

"""
<?xml version='1.0' encoding='UTF-8'?>
<block name="C1">
  <block name="C2">
    <datum alias="Un booléen" name="C1C2a" nature="int" tooltip="actif O/N" group="g1" >
      <value role="default">100</value>
      <value role="current">1</value>
      <value>2</value><value>3</value><value>4</value>
    </datum>
    <datum alias="Un booléen" name="C1C2b" nature="int"/>
    <datum name="C1C2c" extension="C1C2b" />
  </block>
  <datum name="C1b" extension="C2/C1C2a">
    <value role="current">5</value>
  </datum>
  <datum symlink="C2/C1C2a" />
</block>

In this sample, container[u'C1C2a'] and container[u'C2/C1C2a'] refer to same object.
C1b is built using "C2/C1C2a" and adds informations :

container[u'C2/C1C2a'] vaut
Datum(u'C1C2a', int, 1, 100, [1, 2, 3, 4, 100], u'actif O/N', u'g1', u'Un booléen')

container[u'C1b'] vaut
Datum(u'C1C2a', int, 5, 100, [1, 2, 3, 4, 5, 100], u'actif O/N', u'g1', u'Un booléen')

"""

__all__ = [
  'container_to_xml',
  'container_to_node',
  'xml_to_container',
  'node_to_container',
  'key_value_xml_to_udict',
  'key_value_node_to_udict',
  'datum_to_node',
  'node_to_datum',
  'nature_to_xml_attrs',
  ]

from _pylot.minimal.datum import Datum
from _pylot.minimal.container import Container
from _pylot.minimal.utils import list2, check_value
from _pylot.minimal.xmltools import node_to_xml, etree
from _pylot.minimal.pylotdict import PyLotDict
from _pylot.minimal.exception import CustomException



CONTAINER_ATTRS = ('name', 'alias', 'group', 'tooltip')

class ErrCannotSerializeNature(CustomException):
  title = u'Nature %(value)s is not valid'
  message = u"Nature %(value)s is not valid because nature.__name__ is not defined"

class ErrCannotSerializeValue(CustomException):
  title = u'Value %(value)r cannot be serialized using light xml mode'
  message = u"Value %(value)r cannot be serialized using light xml mode because nature is too complex. Use full xml export instead"

class RelationDb(PyLotDict):
  u"""
  Class used to store dependencies between data in xml files.
  First, relationship are stored in RelationDb object.
  Then, resolve method finishes to build container using partially built container
  and RelationDb information. 
  """
  def __init__(self):
    PyLotDict.__init__(self)
    self.current_path = u''
    self._up_path = u''

  def resolve(self, container):
    if self :
      self._resolve(container, self[0], u'')

  def _path(self, parent, path):
    u"""
    >>> db = RelationDb()
    >>> db._path(u'/root/a', u'b/c')
    u'a/b/c'
    """
    parent = unicode(parent)
    path = unicode(path)
    if path.startswith(u'/'):
      paths = path.split(u'/')[2:] # /root/test -> ['', 'root', 'test']
    else :
      paths = parent.split(u'/') + path.split(u'/')
      paths = paths[2:]
    final_path = u'/'.join(paths)
    return final_path

  def _resolve(self, container, dic, parent, mod_attrs=None):
    for k, v in dic.items() :
      if isinstance(v, self.__class__):
        v._resolve(container, v, u'%s/%s' % (parent, k))
      else :
        node, datum_class = v
        if u'extension' in node.attrib :
          path = self._path(parent, node.attrib.pop(u'extension'))
          datum = container[path].copy()
          update_datum_attrs(datum, node, mod_attrs)
          update_datum_values(datum, node)

        elif u'symlink' in node.attrib :
          path = self._path(parent, node.attrib.pop(u'symlink'))
          datum = container[path]

        elif u'copy' in node.attrib :
          path = self._path(parent, node.attrib.pop(u'copy'))
          datum = container[path].copy()

        else :
          continue

        path = self._path(parent, datum.name)
        container[path] = datum

def nature_to_xml_attrs(nature):
  u"""
  Generate attributes for given nature.
  See also :func:`~pyLot.core.xml_attrs_to_type`
  
  raises: ErrCannotSerializeNature if function fails to convert nature.
  
  >>> nature_to_xml_attrs(int)
  {'nature':'int'}
  >>> from pyLot.core import Coordinate
  >>> attrs = nature_to_xml_attrs(int)
  >>> attrs['pymodule']
  '_pylot.minimal.datatype'
  >>> attrs['nature']
  'Coordinate'
  """
  attrs = {}
  if nature is None :
    return attrs
  if nature.__module__ != '__builtin__' :
    attrs['pymodule'] = nature.__module__

  try :
    nature = nature.__name__
  except AttributeError :
    raise ErrCannotSerializeNature(nature)

  # use str instead unicode for backward compatibility and to ease python3 migration 
  if nature == u'unicode' :
    nature = u'str'
  attrs['nature'] = nature
  return attrs

def xml_attrs_to_type(attrs, nature):
  u"""
  Generate nature (python type or class) from given attributes.
  If generation fails, it uses nature as default nature.
  
  See also :func:`~pyLot.core.nature_to_xml_attrs`

  """
  if u'objtype' in attrs :
    if u'objmodule' in attrs :
      module_name = attrs[u'objmodule']
      module = __import__(module_name, fromlist=[module_name])
      objtype = getattr(module, attrs[u'objtype'])
    else :
      raise NameError, "name '%s' is not defined" % attrs[u'objtype']
    return objtype
  else :
    return nature

def container_nature_to_xml_attrs(obj):
  u"""
  Like :func:`pyLot.core.nature_to_xml_attrs` but for container instead of
  datum.
  """
  attrs = {}
  objtype = type(obj)
  if objtype in (Datum, Container):
    pass
  else :
    try :
      objtypename = objtype.__name__
    except AttributeError :
      raise ErrCannotSerializeNature(objtype)
    else :
      attrs['objmodule'] = objtype.__module__
      attrs['objtype'] = objtypename
  return attrs

def datum_to_node(datum, mod_attrs):
  u"""
  Generates an :obj:`~xml.etree.ElementTree.Element` from datum.
  mod_attrs allows to restrict 
  :attr:`datum fields<pyLot.core.Datum.ordered_keys>`.
  
    * nature is exported using :func:`~pyLot.core.nature_to_xml_attrs`.
    * value, default_value and value_list are exported using :func:`~pyLot.core.Datum.valueToXmlElement`.
    * other fields are export as key/values. Ex: Datum.group -> {u'group':u'MyGroup'}
  
  example of output for 
  Datum(u'd', int, 1, 10, [2, 3, 4], group=u'g1', alias=u'Un entier') :

  .. code-block :: xml
  
    <datum alias="Un entier" name="d" nature="int" group="g1" >
      <value role="default">10</value>
      <value role="current">1</value>
      <value>2</value>
      <value>3</value>
      <value>4</value>
    </datum>
  
  :param mod_attrs: exported fields (default: exports all fields)
  """
  attrs = nature_to_xml_attrs(datum.nature)
  for attr in mod_attrs :
    value = getattr(datum, attr)
    if value is None :
      pass
    else :
      attrs[attr] = unicode(value)

  node = etree.Element('datum', attrs)
  if datum.value is not None :
    node.append(datum.valueToXmlElement(datum.value, 'current'))

  if datum.default_value is not None :
    node.append(datum.valueToXmlElement(datum.default_value, 'default'))
  for value in list2(datum.value_list) :
    if value not in [datum.value , datum.default_value] :
      node.append(datum.valueToXmlElement(value))
  return node

def datum_to_node_light(datum, mod_attrs):
  u"""
  Contrary to :func:`~pyLot.core.datum_to_node`, only value is exported.
  All other fields are lost ! Useful to compress file, generally used to
  update a well defined containers with "user values" or "recent values".

  example of output for 
  Datum(u'd', int, 1, 10, [2, 3, 4], group=u'g1', alias=u'Un entier') :

  .. code-block :: xml
  
    <datum name="d" value="1" />

  """
  xml_datum = etree.Element(u'datum')
  if is_complex(datum.value):
    raise ErrCannotSerializeValue(datum.value)
  else :
    value = unicode(datum.value)
  xml_datum.attrib = dict(name=datum.name, value=value)
  return xml_datum

def _container_to_node(container, container_attrs, mod_attrs,
                       datum_to_node):

  default = container.__class__()
  if mod_attrs is None :
    mod_attrs = list(container.datum_type.ordered_keys)
    mod_attrs.remove(u'value')
    mod_attrs.remove(u'nature')
    mod_attrs.remove(u'default_value')
    mod_attrs.remove(u'value_list')

  attrs = nature_to_xml_attrs(container.nature)
  attrs.update(container_nature_to_xml_attrs(container))

  for attr in container_attrs :
    value = getattr(container, attr)
    if value is None or value == getattr(default, attr):
      pass
    else :
      attrs[attr] = unicode(value)

  block = etree.Element('block', attrs)
  for v in container.iterContainerValues() :
    node = _container_to_node(v, container_attrs, mod_attrs, datum_to_node)
    if node is not None:
      block.append(node)
  for v in container.iterDatumValues() :
    node = datum_to_node(v, mod_attrs)
    if node is not None:
      block.append(node)

  return block

def container_to_node(container, mode=u'normal',
                      container_attrs=None, mod_attrs=None):
  u"""
  :param mode: default: normal, available: light
  :param container_attrs: :attr:`Container fields<pyLot.core.Container.ordered_keys>` to export.
  :param mod_attrs: :attr:`Datum fields<pyLot.core.Datum.ordered_keys>` to export.
  """
  check_value(u'mode', mode, [u'normal', u'light'])

  if mode == u'light':
    _datum_to_node = datum_to_node_light
  else :
    _datum_to_node = datum_to_node

  if mod_attrs is None and mode == u'normal':
    mod_attrs = None
  elif mod_attrs is None and mode == u'light':
    mod_attrs = ('name',)

  if container_attrs is None :
    container_attrs = container.ordered_keys

  container_attrs = list(container_attrs)
  if mode == u'light':
    container_attrs.remove(u'tooltip')
    container_attrs.remove(u'alias')
    container_attrs.remove(u'group')

  return _container_to_node(container, container_attrs, mod_attrs,
                            _datum_to_node)

def update_datum_attrs(datum, root, mod_attrs=None, force=True):
  u"""
  force=True : replace all fields even if yet defined in datum
  """
  attrs = root.attrib
  if mod_attrs is None :
    mod_attrs = list(datum.ordered_keys)
    mod_attrs.remove(u'value')
    mod_attrs.remove(u'nature')
    mod_attrs.remove(u'default_value')
    mod_attrs.remove(u'value_list')

  for attr in mod_attrs :
    if force is False and getattr(datum, attr) is not None :
      continue
    try :
      val = attrs[attr]
    except KeyError :
      pass
    else :
      val = val.strip()
      if val :
        setattr(datum, attr, unicode(val))

  try :
    nature = unicode(attrs['nature'])
  except KeyError :
    nature = datum.nature
  try :
    module = unicode(attrs['pymodule'])
  except KeyError :
    module = '__builtin__'

  if module == '__builtin__':
    nature = Datum.getType(nature)
  else :
    module = __import__(module, fromlist=module)
    nature = getattr(module, nature)

  if force is False and datum.nature is not None :
    pass
  else :
    datum.nature = nature

def update_datum_values(datum, root):
  nature = datum.nature
  value_list = []
  for node in list(root) :
    if node.tag != u'value':
      continue
    if node.text is None :
      try :
        value = nature()
      except ValueError :
        if nature is unicode :
          value = u''
        else :
          continue
    else :
      try :
        value = nature.FromXmlElement(node)
      except AttributeError :
        value = unicode(node.text)

    try :
      role = node.attrib['role']
    except KeyError :
      value_list.append(value)
    else :
      if role == u'current' :
        datum.value = value
      elif role == 'default':
        datum.default_value = value
      else:
        value_list.append(value)

    datum.upValueList(value_list)

def node_to_datum(root, mod_attrs, mode=u'normal', datum_class=Datum,
                  db=RelationDb(), parent=None):

  attrs = root.attrib
  if 'extension' in attrs :
    db[parent + u'/' + attrs[u'name']] = (root, datum_class)
    return
  elif 'symlink' in attrs :
    name = attrs[u'symlink'].split(u'/')[-1]
    db[parent + u'/' + name] = (root, datum_class)
    return
  elif 'copy' in attrs :
    name = attrs[u'copy'].split(u'/')[-1]
    db[parent + u'/' + name] = (root, datum_class)
    return

  datum = datum_class()
  update_datum_attrs(datum, root, mod_attrs)

  if mode == u'light':
    try :
      value = attrs[u'value']
    except KeyError :
      pass
    else :
      value = unicode(value).strip()
      if value.lower() == u'none':
        value = None
      elif value.lower() in (u'true', u'on', u'1'):
        value = True
      elif value.lower() in (u'false', u'off', u'0'):
        value = False
      datum.value = value

  else :
    update_datum_values(datum, root)

  return datum

def node_to_container(root, mode=u'normal', container_class=Container,
                      db=RelationDb(), parent=u''):
  if unicode(root.tag) != u'block' :
    return
  container_class = xml_attrs_to_type(root.attrib, container_class)
  if mode == u'normal':
    mod_attrs = None
  elif mode == u'light':
    mod_attrs = ('name',)
  kargs = {}
  for attr in container_class.ordered_keys :
    try :
      val = root.attrib[attr]
    except KeyError :
      pass
    else :
      val = val.strip()
      if val :
        kargs[attr] = unicode(val)

  container = container_class(**kargs)

  for node in list(root):
    if node.tag == 'block' :
      data = node_to_container(node, mode=mode, container_class=container_class,
                               db=db, parent=parent + u'/' + container.name)
      container.addContainer(data)
    elif node.tag == 'datum' :
      datum = node_to_datum(node, mod_attrs, mode=mode,
                            datum_class=container.datum_type,
                            db=db, parent=parent + u'/' + container.name)
      if datum :
        container.addDatum(datum)
    else :
      continue

  return container

def container_to_xml(book, stream, mode=u'normal'):
  u"""
  stream: filepath or stream
  """
  node = container_to_node(book, mode=mode)
  node_to_xml(node, stream)

def xml_to_container(filepath, mode=u'normal', container_class=Container):
  tree = etree.parse(filepath)
  db = RelationDb()
  container = node_to_container(tree.getroot(), mode=mode,
                                container_class=container_class,
                                db=db)

  # resolve dependencies between datum
  db.resolve(container)
  return container

def is_complex(value):
  u"""
  If value.advanced is True, return True else, return False
  
  .. seealso ::
  
    Extending Datum
  """
  try :
    advanced = value.advanced
  except AttributeError :
    return False
  else :
    return advanced

def key_value_xml_to_udict(filepath):
  tree = etree.parse(filepath)
  dic = key_value_node_to_udict(tree.getroot())
  return dic

def key_value_node_to_udict(root):
  dic = {}
  for node in list(root):
    if node.tag == 'block' :
      dic[node.attrib[u'name']] = key_value_node_to_udict(node)
    elif node.tag == 'datum' :
      dic[node.attrib[u'name']] = unicode(node.attrib[u'value'])
    else :
      continue
  return dic
